#include "Servidor.h"
#include <sstream>
#include <map>
#include <string>
#include "Log.h"

Servidor::Servidor(t_puerto puerto) {
    this->socket.setListener(this);
    this->socket.setPuerto(puerto);
    cargarNiveles();
    std::stringstream ss;
    if (this->usuarios.cargar()==0){
        ss << "Usuarios: \n" << usuarios.aString() << std::endl;
        Log::loguear(MINFO, ss.str());
    }
}

Servidor::~Servidor() { }

/* Informacion de los archivos {numero_nivel}.json
    creados con el editor de niveles */
std::map<int, Json::Value *> Servidor::niveles;

bool Servidor::getNivel(int nivel, Json::Value &jsonNivel) {
    if (Servidor::nivelExiste(nivel))
        jsonNivel = *(Servidor::niveles[nivel]);
    else
        return false;
    return true;
}

bool Servidor::nivelExiste(int nivel) {
    return (Servidor::niveles.find(nivel) != Servidor::niveles.end());
}

int Servidor::atender(ServerSocket &socket_server) {
    Socket *socket = socket_server.getSocketAceptado();
    Conexion *conexion = new Conexion(socket, this);
    m_conexiones.lock();
    conexiones.push_back(conexion);
    conexion->start();
    m_conexiones.unlock();
    return 0;
}

bool Servidor::cargarNiveles() {
    Log::loguear(MINFO,"Cargando información de niveles...");
    std::fstream a_nivel;
    Json::Value *jsonNivel;
    int num_nivel;
    std::stringstream ruta_archivo;

    bool continuar = true;
    num_nivel = NIVEL_INICIAL;
    while (num_nivel <= NIVEL_MAXIMO && continuar) {
        ruta_archivo.str("");
        ruta_archivo.clear();
        ruta_archivo << PATH_NIVELES << "/" << num_nivel << ".json";

        if (abrirArchivo(ruta_archivo.str(), a_nivel)) {
            jsonNivel = new Json::Value();
            if (getJsonDeArchivo(a_nivel, *jsonNivel)) {
                Servidor::niveles[num_nivel] = jsonNivel;
            } else {
                continuar = false;
                delete jsonNivel;
                return false;
            }
        } else {
            continuar = false;
        }
        num_nivel++;
    }
    return true;
}

bool Servidor::abrirArchivo(std::string ruta_archivo, std::fstream &a_nivel) {
    a_nivel.open((char *)ruta_archivo.c_str(), std::fstream::in);
    return a_nivel.good();
}

bool Servidor::getJsonDeArchivo(std::fstream &a_nivel, Json::Value &jsonNivel) {
    Json::Reader reader;
    bool parsingSuccessful;

    if (a_nivel.good()) {
        parsingSuccessful = reader.parse(a_nivel, jsonNivel);
        a_nivel.close();

        if (!parsingSuccessful) {
            std::cout << "Failed to parse configuration\n" <<
                      reader.getFormatedErrorMessages();
            return false;
        }
    } else {
        Log::loguear(MDEBUG, "Error: archivo no abierto.");
        return false;
    }
    return true;
}


void Servidor::getInformacionNiveles(int hasta_nivel, Json::Value &niveles) {
  Json::Value nivel_completo, resumen_nivel;
  bool seguir = true;
  int i = NIVEL_INICIAL;
  while (i <= hasta_nivel && seguir) {
    nivel_completo.clear();
    if ((seguir = Servidor::getNivel(i, nivel_completo))) {
     resumen_nivel["nivel"] = i;
     resumen_nivel["maxJugadores"] = nivel_completo["maxJugadores"];
     resumen_nivel["cantColumnas"] = nivel_completo["cantColumnas"];
     resumen_nivel["cantFilas"] = nivel_completo["cantFilas"];
     resumen_nivel["nombreNivel"] = nivel_completo["nombreNivel"];
     resumen_nivel["sonidoMovimiento"] = nivel_completo["sonidoMovimiento"];
     resumen_nivel["sonidoCombinacion"] = nivel_completo["sonidoCombinacion"];
     niveles.append(resumen_nivel);
     i++;
    } else {
     Log::loguear(MDEBUG, "Error: se ha solicitado nivel sin archivo json.");
    }
  }
}

/** Descripcion: verifica la usuario del mensaje.
    * PRE: el mensaje debe tener atributos: nombre
    * POS: en "respuesta" devuelve mensaje de error. En "usuario" devuelve puntero a instancia
    * del usuario en caso de exito, sino NULL.
    * RETORNO:
    0: es autentico
    1: Mensaje mal firmado (HMAC). Recordar setear key en el cliente.
    2: Usuario no existe
*/
int Servidor::autenticarUsuario(Mensaje &mensaje, Usuario **usuario,
                                std::string &respuesta) {
    *usuario = NULL;
    std::string uid = mensaje.data["nombre"].asString();

    m_usuarios.lock();
    int error = this->usuarios.buscar(uid, usuario);
    m_usuarios.unlock();
    if (error == -1) {
      Log::loguear(MDEBUG, "Error: Usuario no existe.");
      respuesta = MENSAJE_AUTH_NO_EXISTE;
      error = 2;
    } else {
      std::string firma_correcta=firmador.generarFirma(mensaje.getJsonString(),
                                   (*usuario)->getPassword());
      std::string firma_cliente = mensaje.getFirma();
      if (firma_correcta != firma_cliente) {
          if (MODO_DEBUG == 1) {
              std::stringstream ss;
              Log::loguear(MDEBUG, "Error: usuario o contraseña inválido.");
              ss << "Firma válida: " << firma_correcta << ", Firma cliente: "<<
                 firma_cliente;
              Log::loguear(MDEBUG, (char *)ss.str().c_str());
          }
          delete *usuario;
          *usuario= NULL;
          respuesta = MENSAJE_AUTH_NOK;
          error = 1;
      } else {
          Log::loguear(MDEBUG, "Usuario autenticado.");
          respuesta = MENSAJE_AUTH_OK;
          error = 0;
      }
    }
    return error;
}


int Servidor::crearPartido(Usuario *usuario, int nivel, int cant_jugadores) {
    int error = 0;
    Partido *partido = new Partido();
    partido->setNumeroNivel(nivel);
    partido->setId(this->partidos.size());
    partido->setJugNecesarios(cant_jugadores);
    m_partidos.lock();
    this->partidos.push_back(partido);
    m_partidos.unlock();
    error = this->unirAPartido(usuario, partido->getId());

    return error;
}

/** Descripcion: retorna en "respuesta" el resumen en formato json (array) 
    * de los partidos de nivel "nivel" que estan aceptando nuevos jugadores.
    * Retorno: 0 -> exito, 1 -> error, no se encontró ningun partido
    */
int Servidor::getPartidosDisponibles(int nivel, Json::Value &j_partidos) {
    int error = 0;
    Json::Value resumen;
    for (size_t i = 0; i < this->partidos.size(); i++) {
      if (partidos[i]->aceptando() && partidos[i]->getNumeroNivel() <= nivel) {
          partidos[i]->getResumenJson(resumen);
          j_partidos["partidos"].append(resumen);
      }
    }
    if (j_partidos["partidos"].size() == 0)
        error = 1;
return error;
}

/** Descripcion: une al jugador "usuario" al partido con id "id".
    * POS:
    *   - se envian todas las notificaciones que correspondan.
    *   - si el partido se llenó iniciará luego de enviar todas las notificaciones.
    * Retorno:
    * 0: exito, jugador unido.
    * 1: error, no existe partido con ese id.
    * 2: último jugador unido, partido comenzara.
    * -1: el jugador ya se encontraba unido.
    * -2: el partido se ha llenado hace instantes.
    * POS: si el partido se llenó, inicia el partido una vez
    * que se enviaron todas las notificaciones a los clientes.
    */
int Servidor::unirAPartido(Usuario *usuario, int id) {
    Partido *partido = this->getPartidoById(id);
    int error = 0;
    if (partido == NULL) {
        error = 1;
        notificarRespuestaUnion(usuario, 0, partido->getId(),
                                "Error: el partido no existe!");
    } else {
        // Bloqueo el partido para evitar conflictos de union.
        partido->bloquear();
        // Verifico si el partido no se llenó previamente.
        if (partido->lleno()) {
            error = -2;
            notificarRespuestaUnion(usuario, 0, partido->getId(),
                                    "Error: el partido se llenó!");
        } else {
            // Si el jugador no estaba unido anteriormente, lo hago.
            if (partido->getJugadorById(usuario->getUID()) != NULL) {
                error = -1;
                notificarRespuestaUnion(usuario, 1, partido->getId(), 
                                            "OK: ya estabas unido!");
            } else {
                usuario->setPartido(partido);
                partido->addJugador(usuario);

                // Si el partido es nuevo se lo mando a todos 
                // (con el creador agregado)
                if (partido->getCantEsperando() == 1) {
                    notificarPartidoNuevo(partido);
                } else {
                    // NOT_TYPE_ANWSER_JOIN exitoso (envia 1 en respuesta)
                    notificarRespuestaUnion(usuario, 1, partido->getId(),
                                "OK: te has unido al partido solicitado!");
                    // Notifico a jugadores en sala que actualicen el partido,
                    // si ya estaba unido no se notifica nada.
                    if (error != -1)
                        notificarAgregarJugador(usuario, partido);
                    // Si llenó el partido, notifico a todos los que estan 
                    // en la sala que lo borren e inicio.
                    if (partido->lleno()) {
                        error = 2;
                        notificarRemoverPartido(partido);
                        // Una vez enviadas todas las notificaciones, inicia 
                        // el partido.
                        partido->iniciar();
                    }
                }
            }
        }
        partido->desbloquear();
    }
    return error;
}

/**
Descripcion: envia notificacion NOT_TYPE_ANWSER_JOIN al usuario pasado
por parametro con el resultado del join.
*/
int Servidor::notificarRespuestaUnion(Usuario *usuario, int respuesta,
                                      int idPartido, std::string detalles) {
    Mensaje msg_envio;
    msg_envio.data["not_type"] = NOT_TYPE_ANWSER_JOIN;
    msg_envio.data["contenido"]["idPartido"] = idPartido;
    msg_envio.data["contenido"]["respuesta"] = respuesta;
    msg_envio.data["contenido"]["detalles"] = detalles;
    return usuario->enviar(msg_envio);
}

/**
Descripcion: envia a todos los jugadores unidos al partido pasado por parametro
un el mensaje NOT_TYPE_ADD_USER.
POS: se excluye al usuario que se unio al mismo, el cual solo recibe 
     NOT_TYPE_ANWSER_JOIN
*/
int Servidor::notificarAgregarJugador(Usuario *usuario, Partido *partido) {
    int error = 0;
    Mensaje msg_envio;
    Usuario *p_usuario;
    msg_envio.data["not_type"] = NOT_TYPE_ADD_USER;
    msg_envio.data["contenido"]["idPartido"] = partido->getId();
    msg_envio.data["contenido"]["nombre"] = usuario->getNombre();
    for (size_t i = 0; i < conexiones.size(); i++) {
        if (conexiones[i]->autenticada()) {
            p_usuario = conexiones[i]->getUsuario();
            // Salteo al usuario que se unió (a él le llega un 
            // NOT_TYPE_ANWSER_JOIN)
            if (p_usuario->getUID() != usuario->getUID()) {
                if ((p_usuario->getPartido() == NULL || 
                    !p_usuario->jugando()) &&
                     p_usuario->getNivel() >= partido->getNumeroNivel())
                    if (p_usuario->enviar(msg_envio) <= 0)
                        error++;
            }
        }
    }
    return error;
}

/**
Descripcion:  envia notificacion NOT_TYPE_ADD_PARTIDA a todos los jugadores
que tengan el nivel necesario.
*/
int Servidor::notificarPartidoNuevo(Partido *partido) {
    Json::Value resumen_partido;
    Mensaje msg_envio;
    partido->getResumenJson(resumen_partido);
    msg_envio.data["not_type"] = NOT_TYPE_ADD_PARTIDA;
    msg_envio.data["contenido"]["partido"] = resumen_partido;
    return notificarAClientesEnHome(msg_envio, partido->getNumeroNivel());
}

/**
Descripcion:  envia notificacion NOT_TYPE_REMOVE_GAME a todos los jugadores
que posiblemente esten mostrando el juego en la sala.
*/
int Servidor::notificarRemoverPartido(Partido *partido) {
    Mensaje msg_envio;
    msg_envio.data["not_type"] = NOT_TYPE_REMOVE_GAME;
    msg_envio.data["contenido"]["idPartido"] = partido->getId();
    int error = notificarAClientesEnHome(msg_envio, partido->getNumeroNivel());
    return error;
}



/** Descripcion: quita al jugador del partido ya sea que este en espera o jugando
      (en caso de no estar en ninguno no hace nada)
      POS: el jugador podra unirse a otro partido. */
int Servidor::quitarDePartido(Usuario *usuario) {
    Partido *partido = usuario->getPartido();
    int error = 1;
    if (partido != NULL) {
        partido->quitarJugador(usuario->getUID());
        if (partido->getCantEsperando() > 0) {
            Log::loguear(MDEBUG, "Abandona un jugador...");
            error = notificarAbandonoDeJugador(partido, usuario);
        } else {
            Log::loguear(MDEBUG, "Eliminando partido sin jugadores...");
            partido->eliminar();
            error = notificarRemoverPartido(partido);
        }
    }
    usuario->quitarDePartido();
    return error;
}

/** Descripcion: notifica a los jugadores en la sala que el jugador ha
    * abandonado el partido en espera. */
int Servidor::notificarAbandonoDeJugador(Partido *partido, Usuario *usuario) {
    Mensaje mensaje;

    mensaje.data["not_type"] = NOT_OUT_USER;
    mensaje.data["idPartido"] = partido->getId();
    mensaje.data["nombre"] = usuario->getNombre();

return this->notificarAClientesEnHome(mensaje, usuario->getNivel(), usuario);
}

/** Descripcion: le envia a todos los jugadores en el home (los que estan
   * autenticados y no jugando) el mensaje pasado por parametro. El mensaje
   * se envia solo a los usuarios con nivel mayor o igual al pasado por 
     parametro.
      Si se pasa un usuario como 3er parametro saltea. Colocar NULL
      Retorno:
      1: error en el envio de al menos un usuario
      0: notificacion enviada con exito a todos los clientes en home . */
int Servidor::notificarAClientesEnHome(Mensaje &mensaje, int nivel,
                                       Usuario *usuario_a_exceptuar) {
    Usuario *u;
    int error = 0;
    for (size_t i = 0; i < conexiones.size(); i++) {
      if (conexiones[i]->autenticada()) {
       u = conexiones[i]->getUsuario();
       // Si no esta esperando en ningun partido o lo esta y puede jugar 
       // ese nivel le envia mensaje.
       if ((u->getPartido() == NULL || !u->jugando()) && 
            u->getNivel() >= nivel)
        /* En caso de no ser un cliente a excluir de la notificacion, 
           dse envia */
        if (usuario_a_exceptuar == NULL || 
            (usuario_a_exceptuar != NULL &&
              usuario_a_exceptuar->getUID() != u->getUID())) {
            if (u->estaConectado())
                if (u->enviar(mensaje) <= 0)
                    error = 1;
        }
      }
    }
    return error;
}

/** Descripcion: inicializa el estado del partido y envia notificacion 
    a los jugadores.
    * Retorno:
    * 1: error, no existe partido
    * 0: exito
*/
int Servidor::iniciarPartido(int idPartido) {
    Partido *partido = this->getPartidoById(idPartido);
    int error = 0;
    if (partido == NULL)
        error = 1;
    else
        partido->iniciar();
    return error;
}

Partido *Servidor::getPartidoById(int id) {
    Partido *partido = NULL;
    for (size_t i = 0; i < this->partidos.size(); i++) {
        if (partidos[i]->getId() == id) {
            partido = partidos[i];
        }
    }
    return partido;
}


int Servidor::responder(const char *mensaje) {
return socket.enviarConAvisoDeLong((char *)mensaje, (uint32_t)strlen(mensaje));
}


int Servidor::responder(std::string mensaje_serial) {
    return socket.enviarConAvisoDeLong((char *)mensaje_serial.c_str(),
                                       (uint32_t)mensaje_serial.length());
}

int Servidor::crearUsuario(Mensaje &msg_user_attr, std::string &respuesta) {
    Usuario usuario;
    usuario.setNombre(msg_user_attr.data["nombre"].asString());
    usuario.setPassword(msg_user_attr.data["password"].asString());
    m_usuarios.lock();
    int error = this->usuarios.guardar(usuario);
    m_usuarios.unlock();
    if (error != -1)
        if (error == 1)
            respuesta = MENSAJE_USUARIO_CREADO;
        else
            respuesta = MENSAJE_USUARIO_EXISTE;
    else
        respuesta = MENSAJE_ERROR;
    return error;
}

/** Descripcion: autentica mensaje enviado por usuario.
      Retorno:
      0: mensaje bien firmado
      1: mensaje mal firmado
      2: usuario no autenticado
      PRE: el mensaje de entrada debe tener el atributo "uid" y estar
      firmado (haber seteado la clave del mensaje antes de serializar)
*/
int Servidor::autenticarMensaje(Mensaje &mensaje, Usuario &usuario) {
  int error = 0;
  std::string firma_server;
  if (!usuario.autenticado()) {
      error = 2;
  } else {
    if ((firma_server = firmador.generarFirma(mensaje.getJsonString(),
                        usuario.getPassword())) != mensaje.getFirma()) {
      if (MODO_DEBUG == 1) {
        std::stringstream s;
        s << "Firma server: " << firma_server << ", Firma cliente: " <<
          mensaje.getFirma() << "(user key: " << usuario.getPassword() << " )";
        Log::loguear(MDEBUG, (char *)s.str().c_str());
      }
      error = 1;
    }
  }
return error;
}

void Servidor::apagar() {
    Log::loguear(MDEBUG, "Apagando servidor...");
    this->socket.apagar();
    this->socket.join();

    m_partidos.lock();
    Log::loguear(MDEBUG, "Eliminando partidos...");
    for (size_t i = 0; i < partidos.size(); i++) {
        if (partidos[i]->ejecutando()) {
            partidos[i]->finalizar();
            partidos[i]->join();
        }
        delete partidos[i];
    }
    m_partidos.unlock();

    m_conexiones.lock();
    Log::loguear(MDEBUG, "Eliminando conexiones...");
    for (size_t i = 0; i < conexiones.size(); i++) {
        if (!conexiones[i]->finalizada()) {
            conexiones[i]->desconectar();
            conexiones[i]->join();
            conexiones[i]->finalizar();
        }
        delete conexiones[i];
    }
    m_conexiones.unlock();

    Log::loguear(MDEBUG, "Eliminando la informacion cargada de niveles...");
    std::map<int, Json::Value *>::iterator it;
    for (it = Servidor::niveles.begin(); it != Servidor::niveles.end(); ++it) {
        delete it->second;
    }
}

int Servidor::iniciar() {
    int error = 0;
    this->socket.start();
    while (!this->socket.inicializado()) {}

    if (!socket.aceptando()) {
        error = 1;
    } else {
        Log::loguear(MDEBUG, "Presione 'q' para salir...");
        char c;
        while (std::cin >> c && c != 'q') {}
    }

    this->apagar();
    this->socket.join();

    return error;
}

